Claude Sonnet
Différentes version de Claude Sonnet :
Journaux liées à cette note :
J'ai découvert class-variance-authority et tailwinds-variants
Jusqu'à présent, pour la gestion conditionnelle des classes CSS dans mes projets ReactJS ou Svelte, j'utilisais clsx.
Pour Svelte en particulier, j'utilise souvent directement le mécanisme conditionnel natif de l'attribut "class" du framework.
Aujourd'hui, dans un projet professionnel ReactJS, #JaiDécouvert la librairie conditionnelle class-variance-authority.
Cette librairie existe depuis debut 2022 et voici un exemple d'utilisation de class-variance-authority :
<script>
import { cva } from 'class-variance-authority';
const button = cva(
'font-semibold rounded', // équivalent au paramètre `base:` utlisé par tailwind-variants
{
variants: {
intent: {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300'
},
size: {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
},
compoundVariants: [
{
intent: 'primary',
size: 'lg',
class: 'uppercase tracking-wide' // <= appliqué seulement si intent="primary" et size="lg"
}
],
defaultVariants: {
intent: 'primary',
size: 'md'
}
}
);
export let intent = 'primary';
export let size = 'md';
</script>
<button class={button({ intent, size })}>
<slot />
</button>
Je trouve cette approche plus élégante que clsx pour des besoins complexes, comme la création d'un design system.
Dans la même famille de librairie, il existe aussi tailwind-merge que j'avais déjà identifié mais sans avoir jamais pris le temps d'étudier. Ses fonctionnalités sont très simples et minimalistes :
Merge Tailwind CSS classes without style conflicts
Claude Sonnet 4.5 m'a fait découvrir tailwind-variants, une alternative à class-variance-authority. Cette lib est spécifique à Tailwind CSS mais offre de meilleures performances et des fonctionnalités supplémentaires par rapport à class-variance-authority.
Le projet a été créé début 2023, soit environ 1 an après class-variance-authority.
Voici un tableau comparant les fonctionnalités de tailwind-variants et class-variance-authority :
La dernière section de la page Tailwind Variants - Comparison explique l'historique de la lib avec class-variance-authority et les raisons qui ont motivé sa création.
Après quelques difficultés, je pense avoir saisi l'intérêt de la fonctionnalité "slots", disponible dans tailwind-variants mais absente de class-variance-authority.
Si un jour je dois créer des composants de design system avancés dans un projet ReactJS ou Svelte, je pense que j'utiliserai tailwind-variants.
J'ai découvert Adminer, alternative à PhpMyAdmin et je me suis intéressé à OPCache
En travaillant sur un playground d'étude de Quadlet, dans le README.md de l'image Docker mariadb, #JaiDécouvert le projet Adminer (https://www.adminer.org) qui semble être l'équivalent de PhpMyAdmin, mais sous la forme d'un fichier unique.
Je découvre aussi que contrairement à PhpMyAdmin, Adminer n'est pas limité à Mysql / MariaDB, il supporte aussi PostgreSQL.
En regardant le dépôt GitHub d'Adminer, je découvre que le gros fichier PHP de 496 kB est le résultat de la concaténation de nombreux fichiers php.
Ça me rassure, parce que je me demandais comment l'édition d'un fichier unique de cette taille pouvait être humainement gérable.
Je trouve astucieux ce mode de déploiement d'un projet PHP sous forme d'un seul fichier qui me fait penser à la méthode Golang. Cependant, je me pose des questions sur la performance de cette technique étant donné que PHP fonctionne en mode process-per request (CGI), ce qui signifie que ce gros fichier PHP est interprété à chaque action sur la page 🤔.
En creusant un peu le sujet avec Claude Sonnet 4.5, je découvre que depuis la version 5.5 de PHP, OPCache améliore significativement la vitesse des requêtes PHP, sans pour autant atteindre celle de Golang, NodeJS, Python ou Ruby qui utilisent des serveurs HTTP intégrés. La consommation mémoire reste supérieure dans des conditions d'implémentation comparables.
Avec OPCache, Adminer semble rester performant malgré l'utilisation d'un fichier unique.
Journal du lundi 01 décembre 2025 à 08:53
J'ai généré avec Claude Sonnet 4.5 le script Python text_to_audio.py. Il me permet de transformer automatiquement du texte en anglais en fichier audio mp3.
J'intègre ensuite ces fichiers dans mes carte-mémoire Anki pour travailler mon anglais.
Installation sous Fedora :
$ mkdir -p ~/.local/bin
$ curl -o ~/.local/bin/text_to_audio.py https://gist.githubusercontent.com/stephane-klein/1406e746f0253956062d4adff7a692bd/raw/8571cdd91cae8ebcd208435daacf431cfc1cd353/text_to_audio.py
$ chmod +x ~/.local/bin/text_to_audio.py
Exemple d'utilisation :
$ text_to_audio.py "Reinforcement Learning from Human Feedback"
Downloading audio for: 'Reinforcement Learning from Human Feedback'
Language: en-GB
URL: https://translate.google.com/translate_tts?ie=UTF-8&tl=en-GB&client=tw-ob&q=Reinforcement+Learning+from+Human+Feedback
Saving to: /home/stephane/english_audio/2025-12-01_Reinforcement_Learning_from_Human_Feedback.mp3
✓ Successfully downloaded to '/home/stephane/english_audio/2025-12-01_Reinforcement_Learning_from_Human_Feedback.mp3'
File size: 28224 bytes
J'utilise actuellement un endpoint HTTP de Google Translator pour générer les fichiers audio, en attendant de trouver une alternative plus open source / communautaire.
Journal du vendredi 21 novembre 2025 à 14:32
Via Claude Sonnet 4.5, #JaiDécouvert le projet Massive Text Embedding Benchmark qui compare les embeddings Models.
Voici le site de documentation, son dépôt GitHub, et son leaderboard qui liste actuellement 319 models, dont 180 supportant le français.
Setup Fedora CoreOS avec LUKS et Tang
Il y a quelques jours, dans ma note "Setup Fedora CoreOS avec LUKS et TPM", je disais :
Une solution pour traiter ce point faible est d'utiliser un pin éloigné physiquement du serveur qui l'utilise.
Le framework Clevis utilise le terme "pins" pour désigner les différents méthodes de déverrouillage d'un volume LUKS.
Origine du mot "pin" ?
Claude Sonnet 4.5 m'a expliqué que le terme "pin", qui se traduit par "goupille" en français, désigne la pièce mécanique qui bloque l'ouverture d'un cadenat.
Par exemple, dans un contexte self hosting dans un homelab, je peux héberger physiquement un serveur dans mon logement et le connecter à un pin sur un serveur Scaleway ou sur un serveur dans le homelab d'un ami.
Les pins distants, accessibles via réseau, sont appelés serveurs Network-Bound Disk Encryption.
Si le serveur Network-Bound Disk Encryption est configuré pour répondre uniquement aux requêtes provenant de l'IP de mon réseau homelab, en cas de vol du serveur, le voleur ne pourra pas récupérer le secret permettant de déchiffrer le volume LUKS.
Dans le playground install-coreos-iso-on-qemu-with-luks-and-tang, j'ai testé avec succès le déverrouillage d'un volume LUKS avec un serveur Network-Bound Disk Encryption nommé tang.
Pour être précis, dans la configuration de ce playground, deux pins sont obligatoires pour déverrouiller automatiquement le volume : un pin tang et un pin TPM2. Le nombre minimum de pins requis pour le déverrouillage est défini par le paramètre threshold.
clevis, qui permet de configurer les pins et de gérer la récupération de la passphrase à partir des pins, utilise l'algorithme Shamir's secret sharing (SSS) pour répartir le secret à plusieurs endroits.
Voici quelques scénarios de conditions de déverrouillage que clevis permet de configurer grâce à SSS :
- TPM2 ou Tang serveur 1
- TPM2 et Tang serveur 1
- Tang serveur 1 ou Tang serveur 2
- 2 parmi Tang serveur 1, Tang serveur 2, Tang serveur 3
- ...
Si les conditions ne sont pas remplies, systemd-ask-password demande à l'utilisateur de saisir sa passphrase au clavier.
Je n'ai pas trouvé d'image docker officielle de tang. Toutefois, j'ai trouvé ici l'image non officielle padhihomelab/tang (son dépôt GitHub : https://github.com/padhi-homelab/docker_tang).
Dans mon playground, je l'ai déployé dans ce docker-compose.yml.
J'ai trouvé la configuration butane de tang simple à définir (lien vers le fichier) :
luks:
- name: var
device: /dev/disk/by-partlabel/var
wipe_volume: true
key_file:
inline: password
clevis:
tpm2: true
tang:
- url: "http://10.0.2.2:1234"
# $ docker compose exec tang jose jwk thp -i /db/pLWwUuLhqqFb-Mgf5iVkwuV4BehG9vzd2SXGMyGroNw.jwk
# pLWwUuLhqqFb-Mgf5iVkwuV4BehG9vzd2SXGMyGroNw
thumbprint: dx9dNzgs-DeXg0SCBQW5rb7WQkSIN1B8MIgcO6WxJfI
threshold: 2 # TMP2 + Tang (or passphrase keyboard input)
La seule complexité que j'ai rencontrée est la méthode pour récupérer le paramètre thumbprint de l'instance tang.
Voici la méthode que j'ai utilisée :
$ docker compose exec tang jose jwk thp -i /db/pLWwUuLhqqFb-Mgf5iVkwuV4BehG9vzd2SXGMyGroNw.jwk
pLWwUuLhqqFb-Mgf5iVkwuV4BehG9vzd2SXGMyGroNw
Autre difficulté, il faut ajouter les arguments kernel suivants pour activer l'accès réseau dès le début du process de boot afin de permettre à clevis d'accéder au serveur tang :
variant: fcos
version: 1.6.0
kernel_arguments:
should_exist:
- ip=dhcp
- rd.neednet=1
J'ai intégré au README.md du playground une section nommée "How to switch from 2 required ping to 1 or the opposite?", pour documenter comment modifier à chaud la configuration clevis.
Par exemple pour changer de serveur tang ou modifier le nombre de pins nécessaires pour déverrouiller la partition chiffrée.
Il est conseillé d'activer le pin TPM2 en complément de tang pour éviter ce type d'attaque décrit par Claude Sonnet 4.5 :
Imaginons un admin malveillant dans ton entreprise :
- Il a accès physique aux serveurs
- Il a accès au réseau (donc au serveur Tang)
- Il clone un disque pendant la nuit
- Il essaie de le monter sur sa propre machine pour extraire des données
Tang seul : ❌ Il réussit
TPM + Tang : ✅ Il échoue
À noter que l'instance tang ne contient pas la passphrase et ne voit jamais passer la passphrase via son API.
Voici les explications que m'a données Claude Sonnet 4.5 :
Tang ne stocke pas la passphrase secrète de chiffrement LUKS. C'est justement tout l'intérêt de cette solution !
Voici comment ça fonctionne :
Le principe de Tang + Clevis
- Génération de clé dérivée : Clevis (le client) contacte le serveur Tang et récupère sa clé publique
- Chiffrement avec la clé de Tang : Clevis utilise cette clé publique pour chiffrer la passphrase LUKS
- Stockage local : La passphrase chiffrée est stockée localement dans l'en-tête LUKS du disque (pas sur le serveur Tang)
- Déchiffrement au boot :
- La machine contacte Tang
- Tang utilise sa clé privée pour aider à déchiffrer
- La passphrase LUKS est reconstituée
- Le disque est déverrouillé
Ce que Tang sait et ne sait pas
- ❌ Tang ne connaît jamais votre passphrase LUKS
- ❌ Tang ne stocke rien concernant vos clés
- ✅ Tang fournit juste un service cryptographique (un oracle de déchiffrement)
- ✅ C'est un serveur sans état (stateless)
C'est du chiffrement asymétrique avec un mécanisme appelé "network-bound disk encryption" : le disque ne peut être déchiffré que si la machine peut contacter le serveur Tang sur le réseau.
Voici quelques ressources supplémentaires au sujet des techniques de déverrouillage automatique des volumes LUKS :
J'ai découvert les types "unknown" et "never" en TypeScript
En TypeScript, dans mon projet professionnel, #JaiDécouvert le type unknown qui ressemble à any mais qui est différent.
Exemple (produit par Claude Sonnet 4.5) avec any :
let value: any;
value.foo.bar(); // No error, even if it crashes at runtime
value.trim(); // No error, even if value is a number
Exemple avec unknown :
let value: unknown;
value.trim(); // Error: Object is of type 'unknown'
// You must narrow the type first
if (typeof value === 'string') {
value.trim(); // OK, TypeScript knows it's a string
}
unknown a été introduit dans la version 3.0 de TypeScript en 2018 : Announcing TypeScript 3.0 - The unknown type.
J'ai trouvé les réponses à cette question StackOverflow intéressantes : 'unknown' vs. 'any'.
C'est peut-être parce que je ne suis pas habitué à la documentation de TypeScript , mais j'ai l'impression que la fonctionnalité unknown n'est pas correctement documentée. Par exemple, je suis surpris de trouver presque rien à son sujet dans la page Everyday-types , ni dans les chapitres "Reference" :

Et rien non plus dans les tutoriels.
Au passage, j'ai aussi découvert le type never.
#JaimeraisUnJour prendre le temps de parcourir la documentation de TypeScript de manière exhaustive. Jusqu'à présent, je n'en ai jamais eu réellement besoin, car je n'ai jamais contribué à de projet écrit en TypeScript. Mais maintenant, cela devient une nécessité pour mon projet professionnel.
Fusion de CoreOS et Atomic Project en 2018
Cette note fait partie de la série de notes : "J'ai étudié et testé CoreOS et je suis tombé dans un rabbit hole 🙈".
Note précédente : "2014-2018 approche alternative avec Atomic Project".
Suite au rachat de la société CoreOS par Red Hat en 2018, les projets CoreOS Container Linux et Fedora Atomic Host ont fusionné en juillet 2019 pour donner Fedora CoreOS.
D'après mon analyse, mise à part ignition, le projet Fedora CoreOS est construit sur les bases de Fedora Atomic Host et n'a gardé de CoreOS Container Linux que le nom "CoreOS".
Cette nouvelle distribution Fedora CoreOS reste atomic et immutable comme l'ancien CoreOS Container Linux, mais utilise désormais rpm-ostree et OSTree (au lieu du système dual partition A/B), et permet le package layering si nécessaire. La philosophie "100% conteneurs" reste encouragée, mais n'est plus une contrainte absolue.
Voici une chronologie sur l'histoire de CoreOS que m'a proposée Claude Sonnet 4.5 :
2013-2017: CoreOS Container Linux
├─ Atomic ✓ (dual partition)
├─ Immutable ✓
└─ Package layering ✗
2014-2018: Fedora/RHEL Atomic Host
├─ Atomic ✓ (OSTree)
├─ Immutable ✓
└─ Package layering ✓ (rpm-ostree)
2018: Rachat CoreOS par Red Hat
2019+: Fedora CoreOS (fusion des deux)
├─ Atomic ✓ (OSTree)
├─ Immutable ✓
├─ Package layering ✓ (possible mais découragé)
└─ Philosophie: conteneurs first, mais flexible
Note suivante : "Quelques outils CoreOS : coreos-installer, graphe de migration et zincati".
Je fais mon retour dans l'écosystème React, j'ai découvert Jotai et Zustand
Dans le code source de mon projet professionnel, #JaiDécouvert la librairie ReactJS nommée Jotai (https://jotai.org).
Les atom de Jotai ressemblent aux fonctionnalités Svelte Store. Jotai permet entre autres d'éviter de faire du props drilling.
Pour en savoir plus sur l'intérêt de Jotai versus "React context (useContext + useState)", je vous conseille la lecture d'introduction de la page Comparison de la documentation Jotai. J'ai trouvé la section "Usage difference" très simple à comprendre.
Cette découverte est une bonne surprise pour moi, car les atom de Jotai reproduisent l'élégance syntaxique des Store de Svelte, ce qui améliore mon confort de développement en ReactJS. #JaiLu ce thread Hacker News en lien avec le sujet : "I like Svelte more than React (it's store management)".
Je tiens toutefois à préciser que si Jotai améliore significativement mon expérience de développeur (DX) avec ReactJS, cela reste une solution de gestion d'état au sein du runtime ReactJS. En comparaison, le compilateur Svelte génère du code optimisé natif qui reste intrinsèquement plus performant à l'exécution.
Exemple Svelte :
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
// Usage dans component
$count // auto-subscription
Exemple ReactJS basé sur Jotai :
import { atom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom(get => get(countAtom) * 2);
// Usage dans component
const [count] = useAtom(countAtom);
J'ai lu la page "Comparison" de Jotai pour mieux comprendre la place qu'a Jotai dans l'écosystème ReactJS.
#JaiDécouvert deux autres librairies développées par la même personne, Daishi Kato : Zustand et Valtio. D'après ce que j'ai compris, Daishi a développé ces librairies dans cet ordre :
- Zustand en juin 2019 - voir "How Zustand Was Born"
- La première version de Jotai en septembre 2020 - voir "How Jotai Was Born"
- La première version de Valtio en mars 2021 - voir "How Valtio Was Born"
J'ai aussi découvert Recoil développé par Facebook, mais d'après son entête GitHub celle-ci semble abandonnée. Une migration de Recoil vers Jotai semble être conseillée.
J'aime beaucoup comment Daishi Kato choisit le nom de ses librairies, la méthode est plutôt simple 🙂 :
Comme mentionné plus haut, Jotai ressemble à Recoil alors que Zustand ressemble à Redux :
Analogy
Jotai is like Recoil. Zustand is like Redux.
...
How to structure state
Jotai state consists of atoms (i.e. bottom-up). Zustand state is one object (i.e. top-down).
Même en lisant la documentation Comparison, j'ai eu de grandes difficulté à comprendre quand préférer Zustand à Jotai.
En lisant la documentation, Jotai me semble toujours plus simple à utiliser que Zustand.
Avec l'aide de Claude Sonnet 4.5, je pense avoir compris quand préférer Zustand à Jotai.
Exemple Zustand
Dans l'exemple Zustand suivant, la fonction addToCart modifie plusieurs parties du state useCartStore en une seule transaction :
import { create } from 'zustand'
const useCartStore = create((set) => ({
user: null,
cart: [],
notifications: [],
addToCart: (product) => set((state) => {
return {
cart: [...state.cart, product],
notifications: (
state.user
? [...state.notifications, { type: 'cart_updated' }]
: state.notifications
)
};
};
}));
Et voici un exemple d'utilisation de addToCart dans un composant :
function ProductCard({ product }) {
// Sélectionner uniquement l'action addToCart
const addToCart = useCartStore((state) => state.addToCart);
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => addToCart(product)}>
Ajouter au panier
</button>
</div>
);
}
Exemple Jotai
Voici une implémentation équivalente basée sur Jotai :
import { atom } from 'jotai';
const userAtom = atom(null);
const cartAtom = atom([]);
const notificationsAtom = atom([]);
export const addToCartAtom = atom(
null,
(get, set, product) => {
const user = get(userAtom);
const cart = get(cartAtom);
const notifications = get(notificationsAtom);
set(cartAtom, [...cart, product]);
if (user) {
set(notificationsAtom, [...notifications, { type: 'cart_updated' }]);
}
}
);
Et voici un exemple d'utilisation de useToCartAtom dans un composant :
import { useSetAtom } from 'jotai';
import { addToCartAtom } from 'addToCartAtom';
function ProductCard({ product }) {
// Récupérer uniquement l'action (pas la valeur)
const addToCart = useSetAtom(addToCartAtom);
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => addToCart(product)}>
Ajouter au panier
</button>
</div>
);
}
Ces deux exemples montrent que Zustand est plus élégant et probablement plus performant que Jotai pour gérer des actions qui conditionnent ou modifient plusieurs parties du state simultanément.
#JaiLu le thread SubReddit ReactJS "What do you use for global state management? " et j'ai remarqué que Zustand semble plutôt populaire.
En rédigeant cette note, j'ai découvert Valtio qui semble être une alternative à MobX. Je prévois d'étudier ces deux librairies dans une future note.
Système de mise à jour d'Android, Chrome OS, MacOS et MS Windows
Cette note fait partie de la série de notes : "J'ai étudié et testé CoreOS et je suis tombé dans un rabbit hole 🙈".
Note précédente : "Ajout de packages dans des distributions atomiques".
Chrome OS et Android implémentent la stratégie de double partition A/B (seamless) system updates.
Cette technologie offre des mises à jour complètement transparentes en arrière-plan et un redémarrage immédiat.
En revanche, contrairement à la solution CoreOS (méthode détaillée dans cette note), cette méthode a pour inconvénient de consommer deux fois plus d'espace de stockage.
MacOS s'appuie sur les snapshots de son filesystem APFS (fonctionnalité qu'offre aussi btrfs). Cela garantit un retour en arrière rapide vers la version antérieure si des problèmes surviennent.
En revanche, l'upgrade se termine durant le reboot, pouvant prendre de 2 à 5 minutes, alors que le redémarrage reste instantané avec Chrome OS, Android, CoreOS ou Fedora Silverblue.
Comme d'habitude, je n'arrive pas à trouver des informations précises sur le fonctionnement interne de MS Windows 😔. D'après Claude Sonnet 4, le système de mise à jour de Windows 10 et Windows 11, baptisé Unified Update Platform (UUP), semble plutôt daté : pas d'A/B (seamless) system updates, absence d'atomicité, installation longue lors du reboot (10 à 30 minutes), possibilité d'échec en cours de processus, rollback complexe, aucun système de snapshot comparable à MacOS. J'ai du mal à croire ce bilan tellement catastrophique, ce qui m'amène à questionner sur l'exactitude des informations rapportées par Claude Sonnet 4.
D'après cette documentation particulièrement riche et mes recherches complémentaires, je pense que la stack libostree + composefs (avec zstd:chunked ) tel qu'implémenté dans Fedora CoreOS est probablement la technologie de mise à jour la plus avancée actuellement disponible.
Avant de présenter le fonctionnement du système de mise à jour de Fedora CoreOS en 2025, je vais retracer l'évolution technique de cette solution.
Note suivante : "CoreOS de 2013 à 2018".
Publication du projet 33 - "POC serveur Git HTTP qui injecte du contenu dans OpenSearch"
Je viens de terminer le "Projet 33 - "POC serveur Git HTTP qui injecte du contenu dans OpenSearch"" en 25h.
Si j'inclus le travail préliminaire du Projet 32 - "POC serveur Git HTTP avec exécution de scripts au push", cela représente 34h au total.
Voici le repository avec le résultat final : https://github.com/stephane-klein/poc-content-repository-git-to-opensearch.
J'ai réussi à implémenter preque tous les éléments que j'avais prévu :
- Un serveur Git HTTP supportant les opérations push et pull
- Après chaque git push, injection automatique des données reçues vers une base de données OpenSearch
- Intégration d'un système de job queue minimaliste qui permet de traiter les tâches d'importation des données Git vers OpenSearch de manière asynchrone. Cela permet entre autres de rendre l'opération git push non bloquante.
- Le modèle de données doit permettre l'accès au contenu de plusieurs branches.
- Upload des fichiers binaires vers un serveur Minio tout concervant leurs metadata (chemin, branche, etc) dans OpenSearch.
- La suppression d'une branche ou d'un commit doit aussi supprimer les données présentes dans OpenSearch et Minio.
- Utilisation de la librairie nodegit.
Le seul élément que je n'ai pas testé est celui-ci :
- L'accès aux données via l'API de OpenSearch ne doit pas être perturbé pendant les phases d'importation de données depuis Git.
Je précise d'emblée que l'implémentation de la fonctionnalité d'exploration web du content repository manque actuellement d'élégance.
Les dossiers suivants contiennent une quantité importante de code dupliqué :
src/routes/[...pathname]/,src/routes/branches/[branch_name]/[...pathname]/- et
src/routes/r/[revision]/[...pathname]/
src/routes
├── branches
│ ├── [branch_name]
│ │ ├── history
│ │ │ ├── +page.server.js
│ │ │ └── +page.svelte
│ │ ├── +page.server.js
│ │ ├── +page.svelte
│ │ └── [...pathname]
│ │ ├── +page.server.js
│ │ └── +page.svelte
│ ├── +page.server.js
│ └── +page.svelte
├── +page.server.js
├── +page.svelte
├── [...pathname]
│ ├── +page.server.js
│ ├── +page.svelte
│ └── raw
│ └── +server.js
└── r
├── +page.server.js
└── [revision]
├── history
│ ├── +page.server.js
│ └── +page.svelte
├── +page.server.js
├── +page.svelte
└── [...pathname]
├── +page.server.js
├── +page.svelte
└── raw
Pour le moment, je n'ai pas encore trouvé comment éviter cette duplication de manière élégante.
J'ai pensé à 3 approches pour améliorer cette implémentation :
- Factoriser la logique de query des fichiers
+page.server.jsdans une fonction partagée. - Migrer complètement ces pages d'exploration vers
src/hooks.server.js(avec les Server hooks de SvelteKit ).
Comme cette partie n'était pas au cœur du projet, j'ai préféré ne pas y investir davantage de temps.
Dans ce projet, j'ai utilisé pour la première fois OpenSearch, le fork de Elasticsearch. J'ai dû faire quelques adaptations par rapport à Elasticsearch mais rien de vraiment complexe.
J'ai utilisé la librairie @opensearch-project/opensearch avec succès, bien aidé par Claude Sonnet 4 pour écrire mes query OpenSearch.
J'aimerais mieux maîtriser l'api de OpenSearch et Elasticsearch, mais je ne les utilise pas suffisamment.
Cette dépendance à un LLM pour écrire ces requêtes me contrarie, je me sens prolétaire et j'ai le sentiment de perdre l'habitude de l'effort. Je pense à cette recherche "Your Brain on ChatGPT: Accumulation of Cognitive Debt when Using an AI Assistant for Essay Writing Task" et cela me préoccupe.
J'ai développé un système de job queue minimaliste en NodeJS avec une persistance basée sur des fichiers json simples : src/lib/server/job-queue.js.
Ma recherche avec Claude Sonnet 4 n'a révélé aucune librairie minimaliste existante qui se contente de fichiers pour la persistance.
Cette implémentation me paraît suffisamment robuste pour répondre à l'objectif que je me suis fixé.
J'ai implémenté la fonction importRevision avec nodegit pour parcourir toutes les entrées d'une révision Git du repository et les importer dans OpenSearch.
Claude Sonnet 4 m'a encore été d'une grande aide, me permettant d'éviter de passer trop de temps dans la documentation d'API de NodeGit, qui reste assez minimaliste.
Mon expérience de 2015 avec git2go sur le projet CmsHub avait été nettement plus laborieuse, à l'époque pré-LLM. Cela dit, j'avais quand même réussi. 🙂
L'implémentation du endpoint /src/routes/post_recieve_hook_url/+server.js n'a pas été très difficile.
J'ai réussi à implémenter le support de git push --force sans trop de difficulté.
Qu'est-ce qui t'a amené à choisir OpenSearch pour ce projet, plutôt qu'un autre type de base de données ?
Suite à de multiples expérimentations durant l'été 2024 (voir 2024-08-17_1253 ou Projet 5), j'ai sélectionné Elasticsearch comme moteur de base de données pour sklein-pkm-engine.
La puissance du moteur de query d'Elasticsearch m'a vraiment séduit, comme on peut le voir dans cette implémentation. Ça me paraît beaucoup plus souple que ce que j'avais développé avec postgres-tags-model-poc.
J'ai donc décidé d'explorer les possibilités d'Elasticsearch ou de son fork OpenSearch comme moteur de base de données de content repository. J'ai décidé d'en faire mon option par défaut tant que je ne rencontre pas d'obstacle majeur ou de point bloquant.
La partie où j'ai le plus hésité concerne le choix du modèle de données OpenSearch pour stocker efficacement le versioning Git.
J'ai décidé d'utiliser deux indexes distincts : files et commits :
await client.indices.create({
index: "files",
body: {
mappings: {
properties: {
content: {
type: "text"
},
mimetype: {
type: 'keyword'
},
commits: {
type: 'object',
dynamic: 'true'
}
}
}
}
});
await client.indices.create({
index: "commits",
body: {
mappings: {
properties: {
index: {
type: 'integer'
},
time: {
type: 'date',
format: 'epoch_second'
},
message: {
type: "text"
},
parents: {
type: 'keyword'
},
entries: {
type: 'object',
dynamic: 'true',
},
branches: {
type: 'keyword'
}
}
}
}
});
Après import des données depuis le repository dummy-content-repository-solar-system, voici ce qu'on trouve dans files :
[
{
_index: 'files',
_id: '2f729046cb0f02820226c1183aa04ab20ceb857d',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'mars.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'mars.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '1be731144f49282c43b5e7827bef986a52723a71',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'venus.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'venus.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/index.md',
a9272695d179e70cca15e89f1632b8fb76112dca: 'terre/index.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'terre/index.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/lune.md',
a9272695d179e70cca15e89f1632b8fb76112dca: 'terre/lune.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'terre/lune.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '97ef5b8f52f85c595bf17fac6cbec856ce80bd4a',
_score: 1,
_source: {
commits: { '4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/terre.jpg' },
mimetype: 'image/jpeg'
}
}
]
et voici un exemple de contenu de commits :
[
{
_index: 'commits',
_id: '7ce2ab6f8d29fec0348342d95bfe71899dcb44fa',
_score: 1,
_source: { index: 1, time: 1757420855, branches: [ 'main' ], parents: [] }
},
{
_index: 'commits',
_id: '4da69e469145fe5603e57b9e22889738d066a5e2',
_score: 1,
_source: {
entries: {
'venus.md': {
oid: '1be731144f49282c43b5e7827bef986a52723a71',
contentType: 'text/markdown; charset=utf-8'
},
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'mars.md': {
oid: '2f729046cb0f02820226c1183aa04ab20ceb857d',
contentType: 'text/markdown; charset=utf-8'
},
'terre/terre.jpg': {
oid: '97ef5b8f52f85c595bf17fac6cbec856ce80bd4a',
contentType: 'image/jpeg'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 4,
time: 1757429173,
branches: [ 'main' ],
parents: [ 'd9bffc3da0c91366dda54fefa01383b109554054' ]
}
},
{
_index: 'commits',
_id: 'd9bffc3da0c91366dda54fefa01383b109554054',
_score: 1,
_source: {
entries: {
'venus.md': {
oid: '1be731144f49282c43b5e7827bef986a52723a71',
contentType: 'text/markdown; charset=utf-8'
},
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'mars.md': {
oid: '2f729046cb0f02820226c1183aa04ab20ceb857d',
contentType: 'text/markdown; charset=utf-8'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 3,
time: 1757421171,
branches: [ 'main' ],
parents: [ 'a9272695d179e70cca15e89f1632b8fb76112dca' ]
}
},
{
_index: 'commits',
_id: 'a9272695d179e70cca15e89f1632b8fb76112dca',
_score: 1,
_source: {
entries: {
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 2,
time: 1757420956,
branches: [ 'main' ],
parents: [ '7ce2ab6f8d29fec0348342d95bfe71899dcb44fa' ]
}
}
]
Ensuite, je mise beaucoup sur la puissance du moteur de requête d'OpenSearch pour récupérer efficacement les données à afficher.
Voici l'exemple de src/routes/[...pathname]/+page.server.js qui permet d'afficher le contenu d'un fichier de la branche main.
Première requête :
const responseOid = await client().search({
index: 'commits',
body: {
query: {
bool: {
must: [
{
term: {
branches: 'main'
}
},
{
exists: {
field: `entries.${params.pathname}`
}
}
]
}
},
_source: [`entries.${params.pathname}`]
}
});
Seconde requête qui utilise la réponse de la première :
const responseFile = await client().get({
index: 'files',
id: responseOid.body.hits.hits[0]._source.entries[params.pathname].oid,
_source: ['content', 'mimetype']
});
Basé sur l'expérience de ce projet, je souhaite améliorer sklein-pkm-engine pour permettre la mise à jour de notes.sklein.xyz avec mes données locales uniquement via git push, sans avoir besoin d'installer quoi que ce soit sur ma workstation.
Je pense que cette implémentation sera bien plus simple que le Projet 33, car je ne prévois pas d'inclure le support dans un premier temps. Peut-être que je supporterai les branches dans un second temps.
